NestJS と Passport.js を組み合わせて JWT 認証を実現する
インストール
code:sh
$ npm i @nest/passport passport passport-local @nest/jwt passport-jwt
password: Passport.js 本体
password-local: メールアドレス / パスワード認証用の実装ライブラリ
@nest/jwt: JWT 認証用の NestJS 拡張ライブラリ passport-jwt: JWT 認証用の Passport.js 拡張ライブラリ
code:sh
$ npm i --save-dev @types/passport-local @types/passport-jwt
事前準備
ファイル生成
code:sh
$ nest g module auth
$ nest g service auth --no-spec
$ nest g resolver auth --no-spec
PassportModule と JwtModule のインポート
code:ts
@Module({
imports: [
UserModule,
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '1h' },
}),
],
})
Service の実装
code:ts
@Injectable()
export class AuthService {
constructor(
private readonly userService: UserService,
private readonly jwtService: JwtService,
) {}
async validateUser(email: string, password: string): Promise<User | null> {
const user = await this.userService.getUser(email);
if (!user || !(await becrypt.compare(password, user.password))) {
return null;
}
return user;
}
async signIn(user: User): Promise<SignInResponse> {
const payload: JwtPayload = { email: user.email, sub: user.id };
return { accessToken: this.jwtService.sign(payload), user };
}
}
validateUser は渡ってきた認証データが正しいかチェックするためのメソッドで、Strategy の実装時に用いる
Strategy の実装: passport-local
code:ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-local';
import { AuthService } from '../auth.service';
import { User } from '@prisma/client';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({ usernameField: 'email' });
}
async validate(email: string, password: string): Promise<User> {
const user = await this.authService.validateUser(email, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
@Injectable にしないと正常に動作しない
(一敗) radish-miyazaki.icon
validate : PassportStrategy で定義されている抽象メソッド
検証時に呼び出されるため実装する必要がある
作成したクラスは auth 配下の Module にセットする必要がある
code:ts
@Module({
...
})
export class AuthModule {}
Guard の実装
code:ts
import { ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport';
export class GqlAuthGuard extends AuthGuard('local') {
constructor() {
super();
}
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
const request = ctx.getContext();
request.body = ctx.getArgs().signInInput;
return request;
}
}
code:ts
@Resolver()
export class AuthResolver {
constructor(private readonly authService: AuthService) {}
@Mutation(() => SignInResponse)
@UseGuards(GqlAuthGuard)
async signIn(
@Args('signInInput') signInInput: SignInInput,
@Context() context: any,
) {
return await this.authService.signIn(context.user);
}
}
@UseGuards に GqlAuthGuard を渡すことで、signIn が実行される前に validate 処理が実行される
成功した場合のみ signIn が実行される
signIn の関数の引数名は getRequest の以下の部分と一致させる必要がある
code:ts
request.body = ctx.getArgs().signInInput;
(一敗)radish-miyazaki.icon
@Context を用いると、リクエスト内に格納されているコンテキストを引数で受け取れる
この中に User のインスタンスが格納されている
ある Resolver をログイン中のユーザのみアクセスできるようにする
Strategy の実装: passport-jwt
code:ts
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UserService } from 'src/user/user.service';
import { JwtPayload } from '../types/jwtPayload.type';
import { User } from '@prisma/client';
import { Injectable } from '@nestjs/common';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly userService: UserService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET,
});
}
async validate(payload: JwtPayload): Promise<User | null> {
return await this.userService.getUser(payload.email);
}
}
作成したクラスは auth 配下の Module にセットする必要がある
code:ts
@Module({
...
})
export class AuthModule {}
Guard の実装
code:ts
import { ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport';
export class JwtAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
Resolver への適用
@UseGuards に作成した Guard クラスを渡す
code:ts
@Resolver()
export class UserResolver {
constructor(private readonly userService: UserService) {}
@Query(() => UserModel, { nullable: true })
@UseGuards(JwtAuthGuard)
async getUser(@Args() getUserArgs: GetUserArgs): Promise<User> {
...
}
}
参考